package jge.datatypes.math;

import java.util.Arrays;

import jge.exceptions.InvalidMatrixDimensionsException;
import jge.exceptions.SingularMatrixException;

/**
 * Represents a 4x4 Affine Transformation Matrix. Contains methods which act upon the instanced matrix
 * as well as static methods used for generating specific matrices.
 * @author Sam Pengilly
 * @version 1.0
 */
public final class JGEMatrix {
	
	/**
	 * Internal representation of the matrix
	 */
	private float[][] data = new float[4][4];
	
	/**
	 * Default Constructor for new Matrix Instances
	 * @param data - a 4x4 array of values representing the matrix
	 * @throws InvalidMatrixDimensionsException when data argument dimensions are not 4x4
	 * @category Constructor
	 */
	public JGEMatrix(float[][] data) throws InvalidMatrixDimensionsException {
		if (data.length != 4 || data[0].length != 4) {
			throw new InvalidMatrixDimensionsException();
		}
		
		this.data = data;
	}
	
	/**
	 * Clone method
	 * @param matrix - the matrix to copy
	 * @category Instance Method
	 */
	public JGEMatrix clone() {
		float[][] array = new float[4][4];
		float[][] old = this.toArray();
		
		array[0] = old[0].clone();
		array[1] = old[1].clone();
		array[2] = old[2].clone();
		array[3] = old[3].clone();
		
		try {
			return new JGEMatrix(array);
		} catch (InvalidMatrixDimensionsException e) {
			// SHOULD NOT FAIL!
			return null;
		}
	}

	/**
	 * Returns the value at the specified index in the matrix. <br>
	 * Matrix index values must be between 1 and 4 inclusive
	 * @param x - the row index of the matrix 
	 * @param y - the column index of the matrix
	 * @return the value at the specified index
	 * @throws IndexOutOfBoundsException when index values are outside allowable range
	 * @category Instance Method
	 */
	public float get(int x, int y) throws IndexOutOfBoundsException {
		if (x > 4 || x < 1 || y > 4 || y < 1) {
			throw new IndexOutOfBoundsException();
		}
		
		return this.data[x - 1][y - 1];
	}
	
	/**
	 * Sets the value of the specified matrix index to the specified value
	 * @param x - the x index (between 1 and 4 inclusive)
	 * @param y - the y index (between 1 and 4 inclusive)
	 * @param val - the new value for the matrix position
	 * @throws IndexOutOfBoundsException when index values are outside allowable range
	 * @category Instance Method
	 */
	public void set(int x, int y, float val) throws IndexOutOfBoundsException {
		if (x > 4 || x < 1 || y > 4 || y < 1) {
			throw new IndexOutOfBoundsException();
		}
		
		this.data[x - 1][y - 1] = val;
	}
	
	/**
	 * Sets the values of the matrix to those of the provided array
	 * @param data - a 4x4 array of values
	 * @throws InvalidMatrixDimensionsException when data argument dimensions are not 4x4
	 * @category Instance Method
	 */
	public void set(float[][] data) throws InvalidMatrixDimensionsException {
		if (data.length != 4 || data[0].length != 4) {
			throw new InvalidMatrixDimensionsException();
		}
		
		this.data = data;
	}

	/**
	 * Multiplies the instance matrix by the provided matrix m
	 * @param m - the matrix by which to multiply this matrix
	 * @category Instance Method
	 */
	public void multiply(JGEMatrix m) {
		float[][] temp = new float[4][4];
		
		for (int i = 1; i < 5; i++) {
			for (int j = 1; j < 5; j++) {
				temp[i-1][j-1] = this.get(i, 1) * m.get(1, j) + 
							 	 this.get(i, 2) * m.get(2, j) + 
							 	 this.get(i, 3) * m.get(3, j) + 
							 	 this.get(i, 4) * m.get(4, j);
			}
		}
		
		try {
			this.set(temp);
		} catch (InvalidMatrixDimensionsException e) {
			// SHOULD NOT FAIL!
			return;
		}
	}
	
	/**
	 * Returns the Determinant of the matrix
	 * @return the determinant
	 * @category Instance Method
	 */
	public float determinant() {
		float result = 0;
		
		for (int i = 0; i < 4; i++) {
			float temp[][] = new float[3][3];
			
			for (int j = 1;  j < 4; j++) {
				System.arraycopy(this.data[j], 0, temp[j-1], 0, i);
				System.arraycopy(this.data[j], i+1, temp[j-1], i, 3-i);
			}
			
			result += this.data[0][i] * Math.pow(-1, i) * determinant(temp);
		}
		
		return result;
	}
	
	/**
	 * Private recursive helper method for {@link jge.datatypes.math.JGEMatrix#determinant()}
	 * @param mat - A 2 dimensional matrix
	 * @return the determinant of the provided matrix
	 * @category Helper Method
	 */
	private float determinant(float[][] mat) {
		float result = 0;
		
		if(mat.length == 1) {
			result = mat[0][0];
			return result;
		}

		if(mat.length == 2) {
			result = mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0];
			return result;
		}

		for(int i = 0; i < mat[0].length; i++) {
			float temp[][] = new float[mat.length - 1][mat[0].length - 1];

			for(int j = 1; j < mat.length; j++) {
				System.arraycopy(mat[j], 0, temp[j-1], 0, i);
				System.arraycopy(mat[j], i+1, temp[j-1], i, mat[0].length-i-1);
			}

			result += mat[0][i] * Math.pow(-1, i) * determinant(temp);
		}

		return result; 
	}
	
	/**
	 * Inverts the Matrix
	 * @throws SingularMatrixException when the determinant is zero (i.e. the matrix is singular)
	 * @category Instance Method
	 */
	public void invert() throws SingularMatrixException {
		if (this.determinant() == 0) {
			throw new SingularMatrixException();
		}
		
		// TODO: Implement Matrix Inversion Method
	}
	
	/**
	 * Transposes the matrix
	 * @category Instance Method
	 */
	public void transpose() {
		JGEMatrix temp = this.clone();

		for (int i = 1; i < 5; i++) {
			for (int j = 1; j < 5; j++) {
				this.set(j, i, temp.get(i, j));
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object other) {
		if (!(other instanceof JGEMatrix)) {
			return false;
		}
		return Arrays.deepEquals(this.data, ((JGEMatrix) other).toArray());
	}
	
	/**
	 * Returns the data array representing the matrix
	 * @return the 2-dimensional float array
	 * @category Instance Method
	 */
	public float[][] toArray() {
		return this.data;
	}
	
	public String toString() {
		StringBuilder result = new StringBuilder();
		String NEW_LINE = System.getProperty("line.separator");
		
		result.append(this.getClass().getName() + " {" + NEW_LINE);
		result.append("\t{\t" + this.get(1, 1) + ",\t" + this.get(1, 2) + ",\t" + this.get(1, 3) + ",\t" + this.get(1, 4) + "\t}," + NEW_LINE);
		result.append("\t{\t" + this.get(2, 1) + ",\t" + this.get(2, 2) + ",\t" + this.get(2, 3) + ",\t" + this.get(2, 4) + "\t}," + NEW_LINE);
		result.append("\t{\t" + this.get(3, 1) + ",\t" + this.get(3, 2) + ",\t" + this.get(3, 3) + ",\t" + this.get(3, 4) + "\t}," + NEW_LINE);
		result.append("\t{\t" + this.get(4, 1) + ",\t" + this.get(4, 2) + ",\t" + this.get(4, 3) + ",\t" + this.get(4, 4) + "\t}," + NEW_LINE);
		result.append("}");
		
		return result.toString();
	}
	
	// ================================================
	// 				Generator Methods
	// ================================================
	
	/**
	 * Translates the matrix instance by the coordinates specified
	 * @param x - The X-Coordinate by which to translate the matrix
	 * @param y - The Y-Coordinate by which to translate the matrix
	 * @param z - The Z-Coordinate by which to translate the matrix
	 * @category Generator Method
	 */
	public static JGEMatrix translation(float x, float y, float z) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(1, 4, x);
		mat.set(2, 4, y);
		mat.set(3, 4, z);
		
		return mat;
	}

	/**
	 * Translates the matrix instance by a given vector
	 * @param v - the vector by which to translate
	 * @category Generator Method
	 */
	public static JGEMatrix translation(JGEVector v) {
		return JGEMatrix.translation(v.getX(), v.getY(), v.getZ());
	}

	/**
	 * Rotates the Matrix around the X-Axis
	 * @param angle - The angle by which to rotate
	 * @category Generator Method
	 */
	public static JGEMatrix rotationX(float angle) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(2, 2, (float) Math.cos(angle));
		mat.set(2, 3, (float) -Math.sin(angle));
		mat.set(3, 2, (float) Math.sin(angle));
		mat.set(3, 3, (float) Math.cos(angle));
		
		return mat;
	}
	
	/**
	 * Rotates the Matrix around the Y-Axis
	 * @param angle - The angle by which to rotate
	 * @category Generator Method
	 */
	public static JGEMatrix rotationY(float angle) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(1, 1, (float) Math.cos(angle));
		mat.set(1, 3, (float) Math.sin(angle));
		mat.set(3, 1, (float) -Math.sin(angle));
		mat.set(3, 3, (float) Math.cos(angle));
		
		return mat;
	}
	
	/**
	 * Rotates the Matrix around the Z-Axis
	 * @param angle - The angle by which to rotate
	 * @category Generator Method
	 */
	public static JGEMatrix rotationZ(float angle) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(1, 1, (float) Math.cos(angle));
		mat.set(1, 2, (float) -Math.sin(angle));
		mat.set(2, 1, (float) Math.sin(angle));
		mat.set(2, 2, (float) Math.cos(angle));
		
		return mat;
	}

	/**
	 * Rotate the Matrix around all 3 axis by the amounts defined in the provided vector
	 * @param v - the vector that defines the rotation angles for each axis
	 * @return the rotation transformation matrix
	 * @category Generator Method
	 */
	public static JGEMatrix rotation(JGEVector v) {
		JGEMatrix matX = JGEMatrix.rotationX(v.getX());
		JGEMatrix matY = JGEMatrix.rotationY(v.getY());
		JGEMatrix matZ = JGEMatrix.rotationZ(v.getY());
		
		matX.multiply(matY);
		matX.multiply(matZ);
		return matX;
	}

	/**
	 * Scales the Matrix along the X-Axis
	 * @param factor - the value by which to scale
	 * @return the scale transformation matrix
	 * @category Generator Method
	 */
	public static JGEMatrix scaleX(float factor) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(1, 1, factor);
		
		return mat;
	}

	/**
	 * Scales the Matrix along the Y-Axis
	 * @param factor - the value by which to scale
	 * @return the scale transformation matrix
	 * @category Generator Method
	 */
	public static JGEMatrix scaleY(float factor) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(2, 2, factor);
		
		return mat;
	}

	/**
	 * Scales the Matrix along the Z-Axis
	 * @param factor - the value by which to scale
	 * @return the scale transformation matrix
	 * @category Generator Method
	 */
	public static JGEMatrix scaleZ(float factor) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(3, 3, factor);
		
		return mat;
	}
	
	/**
	 * Scales the Matrix according to the values in the provided vector
	 * @param v - the vector detailing the scale operation
	 * @return the scale transformation matrix
	 * @category Generator Method
	 */
	public static JGEMatrix scale(JGEVector v) {
		JGEMatrix mat = JGEMatrix.identity();
		mat.set(1, 1, v.getX());
		mat.set(2, 2, v.getY());
		mat.set(3, 3, v.getZ());
		
		return mat;
	}
	
	/**
	 * Generates and returns a Zero-filled Matrix
	 * @return a Zero-filled matrix
	 * @category Generator Method
	 */
	public static JGEMatrix zero() {
		try {
			return new JGEMatrix(new float[][] {{0, 0, 0, 0}, 
												{0, 0, 0, 0}, 
												{0, 0, 0, 0}, 
												{0, 0, 0, 0}});
		} catch (InvalidMatrixDimensionsException e) {
			// SHOULD NEVER FAIL!!
			return null;
		}
	}
	
	/**
	 * Generates and returns an identity matrix
	 * @return an identity matrix
	 * @category Generator Method
	 */
	public static JGEMatrix identity() {
		try {
			return new JGEMatrix(new float[][] {{1, 0, 0, 0}, 
												{0, 1, 0, 0}, 
												{0, 0, 1, 0}, 
												{0, 0, 0, 1}});
		} catch (InvalidMatrixDimensionsException e) {
			// SHOULD NEVER FAIL!!
			return null;
		}
	}
}